export * from './assert'
export * from './types'
export * from './string'
export * from './font'
export * from './CircularList'

import { assert, assertString, assertObject, assertArray } from './assert'
import { dashToCamel, camelToDash } from './string'
import { isString, isArray, isObject } from './types'
import type { FilterFunctions, RemainMutable } from './types'

/**
 * CSS 样式对象，可选包含：
 *     1. CssStyleDeclaration 的所有可变属性。
 *     2. 所有以 '-' 开头的属性。
 */
export interface CssObject extends Partial<RemainMutable<FilterFunctions<CSSStyleDeclaration>>> {
    [key: `-${string}`]: string
}

/**
 * 将 CSS 字符串解析成 CSS 属性对象。
 * 
 * @param text CSS 属性字符串
 * @returns CSS 属性对象
 */
export function parseCssText(text: string): CssObject {
    const result: CssObject = {}
    const lines = text.split(';')

    for (let line of lines) {
        line = line.trim()
        if (! line.length)
            continue
        const [k, v] = line.split(':')
        result[dashToCamel(k!.trim()) as keyof CssObject] = v?.trim() || ''
    }
    
    return result
}

/**
 * 将 CSS 属性对象装配成 CSS 属性字符串。
 * 
 * @param {CssObject} style 一个 CSS 属性对象
 * @return {string} CSS 字符串
 */
export function buildCssText(style: CssObject): string {
    const result: string[] = []

    for (const [key, value] of Object.entries(style)) {
        result.push(camelToDash(key))
        result.push(':')
        result.push(value as string)
        result.push(';')
    }

    return result.join('')
}

/**
 * 快捷创建一个 DOM 元素，并设置其 HTML 属性（Attribute）和子元素。有多种灵活的
 * 使用方式。如：
 * 1. `createElement(tagName)` 简单创建元素；
 * 2. `createElement(tagName, className)` 创建元素并设置其 CSS 类；
 * 3. `createElement(tagName, className, attrs)` 创建元素并设置其 CSS 类和其他属性；
 * 4. `createElement(tagName, className, attrs, children)` 创建元素并设置其 CSS 类、其他属性和子元素；
 * 5. `createElement(tagName, className, children)` 创建元素并设置其 CSS 类和子元素；
 * 6. `createElement(tagName, attrs)` 创建元素并设置其属性；
 * 7. `createElement(tagName, attrs, children)` 创建元素并设置其属性和子元素；
 * 8. `createElement(tagName, children)` 创建元素并设置其子元素；
 * 
 * @param tagName 元素标签名
 * @param {Optional<string>} className 该元素的 CSS 类名
 * @param {Optional<Record<string, string>>} attrs 元素的 HTML 属性
 * @param {Optional<Node[]>} children 元素的子元素
 * @returns {Element} 创建的 DOM 元素
 */
export function createElement<T extends keyof HTMLElementTagNameMap>(tagName: T, className?: string | Record<string, string> | Node[] , attrs?: Record<string, string> | Node[], children?: Node[]): HTMLElementTagNameMap[T];
export function createElement(tagName: string, className?: string | Record<string, string> | Node[] , attrs?: Record<string, string> | Node[], children?: Node[]): Element {
    if (isArray(attrs)) {
        children = attrs
        attrs = undefined
    }

    if (isObject(className)) {
        attrs = className
        className = undefined
    }
    else if (isArray(className)) {
        children = className
        attrs = undefined
        className = undefined
    }

    const el = document.createElement(tagName)
    if (className) {
        assertString(className)
        el.className = className
    }

    if (attrs) {
        assertObject(attrs)
        for (let key in attrs) {
            if (key === 'className')
                el.className = attrs[key]!
            else if (key === 'style')
                el.style.cssText = attrs[key] as string
            else
                el.setAttribute(key, attrs[key] as string)
        }
    }
    
    if (children) {
        assertArray(children)
        for (let child of children) {
            assert(isString(child) || child instanceof Node)
            el.append(child)
        }
    }

    return el
}

/**
 * Clickout 事件对象。
 * 
 * 继承自 CustomEvent，在其 detail 属性中保存了导致本事件触发的 pointerdown 事件对象。
 */
export class ClickoutEvent extends CustomEvent<PointerEvent> {
    constructor(detail: PointerEvent) {
        super('clickout', { detail: detail })
    }
}

/**
 * 保存被注册过、需要响应 clickout 事件的页面元素。
 */
let _clickoutWatchers: HTMLElement[] = []

/**
 * 设置 window 对象侦听器，在需要的时候向被注册过的元素发送 clickout 事件。
 */
window.addEventListener('pointerdown', (e) => {
    const path = e.composedPath()
    let rest = _clickoutWatchers
    for (let et of path)
        rest = rest.filter(v => (v !== et))

    const clickout = new ClickoutEvent(e)
    rest.forEach(el => el.dispatchEvent(clickout))
}, true)


declare global {
    /**
     * 定义包含了 clickout 事件的事件类型映射表和侦听器映射表。
     */
    interface ShEventMap extends HTMLElementEventMap {
        default: Event
        clickout: ClickoutEvent
    }
    interface ShEventListener<K extends keyof ShEventMap> {
        (e: ShEventMap[K]): void
    }

    /**
     * 为 HTMLElement 类增加快捷方法。
     */
    interface HTMLElement {
        isAncestorOf(node: Node): boolean
        locationInViewport(): { left: number, top: number }
        locationInPage(): { left: number, top: number }
        isPointInside(x: number, y: number): boolean

        addClass(className: string): void
        removeClass(className: string): void
        setClass(className: string): void
        hasClass(className: string): boolean
        toggleClass(className: string, condition?: boolean): void

        updateStyle(style: string | CssObject): void
        clearStyle(): void

        on<K extends keyof ShEventMap>(event: K, listener: ShEventListener<K>, options?: boolean | AddEventListenerOptions): ShEventListener<K>
        on(event: string, listener: ShEventListener<'default'>, options?: boolean | AddEventListenerOptions): ShEventListener<'default'>

        off<K extends keyof ShEventMap>(event: K, listener: ShEventListener<K>, options?: boolean | EventListenerOptions): ShEventListener<K>
        off(event: string, listener: ShEventListener<'default'>, options?: boolean | EventListenerOptions): void

        enableClickout(): void
        disableClickout(): void
    }
}
Object.assign(HTMLElement.prototype, {
    /**
     * 判断是否自己是指定元素的祖先。
     * 
     * @param el 要判断的元素
     * @returns 是自己的祖先则返回 true，否则返回 false
     */
    isAncestorOf: function(node: Node): boolean {
        let pn = node.parentNode
        while (pn !== null && pn !== this as unknown as Node)
            pn = pn.parentNode

        return pn === this as unknown as Node
    },
    /**
     * 返回元素相对于浏览器视口的绝对位置。
     * 
     * @returns 元素的相对于当前文档的 left 和 top
     */
    locationInViewport: function(): {left: number, top: number} {
        const r = (this as unknown as HTMLElement).getBoundingClientRect()
        return { left: r.left, top: r.top }
    },
    /**
     * 返回元素相对于当前文档的绝对位置。
     *
     * @returns 元素的相对于当前文档的 left 和 top
     */
    locationInPage: function(): {left: number, top: number} {
        const r = (this as unknown as HTMLElement).getBoundingClientRect()
        return { left: r.left + window.scrollX, top: r.top + window.scrollY }
    },
    /**
     * 判断一个视口坐标点是否在元素内部。
     * 
     * @param x 横坐标
     * @param y 纵坐标
     * @returns 坐标点在元素内部则返回 true，否则返回 false
     */
    isPointInside: function(x: number, y: number): boolean {
        const r = (this as unknown as HTMLElement).getBoundingClientRect()
        return (r.left <= x && x < r.right && r.top <= y && y < r.bottom)
    },
    /**
     * 向元素的 classList 中增加一个 CSS 类。
     * 
     * @param className 要增加的 CSS 类名
     */
    addClass: function(className: string) {
        (this as unknown as HTMLElement).classList.add(className)
    },
    /**
     * 从元素的 classList 中删除一个 CSS 类。
     * 
     * @param className 要删除的 CSS 类名
     */
    removeClass: function(className: string) {
        (this as unknown as HTMLElement).classList.remove(className)
    },
    /**
     * 设置元素的 CSS 类名。
     * 
     * @param className 要设置的 CSS 类名；如果有多个类名，用空格分隔
     */
    setClass: function(className: string) {
        (this as unknown as HTMLElement).className = className
    },
    /**
     * 判断一个元素是否有指定的 CSS 类。
     *
     * @param {string} className 要判断的 CSS 类名
     * @returns {boolean} 元素有指定的 CSS 类则返回 true，否则返回 false
     */
    hasClass: function (className: string): boolean {
        return (this as unknown as HTMLElement).classList.contains(className)
    },
    /**
     * 根据条件切换指定 CSS 类是否存在。
     * 
     * @param className 要增加或删除的 CSS 类名
     * @param condition 条件
     */
    toggleClass: function(className: string, condition?: boolean) {
        if (condition === undefined)
            condition = (this as unknown as HTMLElement).classList.contains(className)

        if (condition)
            (this as unknown as HTMLElement).classList.add(className)
        else
            (this as unknown as HTMLElement).classList.remove(className)
    },
    /**
     * 更新元素的 CSS 属性。要更新的属性如果在元素的 style 中存在，则将会被替换，否则就新增.
     * 元素的 style 中其他的属性将得以保留。
     * 
     * @param newStyle 要设置的 CSS 属性，用文本串或属性对象形式均可
     */
    updateStyle: function(newStyle: string | CssObject) {
        if (newStyle === '' || Object.keys(newStyle).length === 0)
            return

        if (isString(newStyle))
            newStyle = parseCssText(newStyle as string)

        const currStyle = parseCssText((this as unknown as HTMLElement).style.cssText)
        Object.assign(currStyle, newStyle);
        (this as unknown as HTMLElement).style.cssText = buildCssText(currStyle)
    },
    /**
     * 清空元素的 CSS 属性。
     */
    clearStyle: function() {
        (this as unknown as HTMLElement).style.cssText = ''
    },
    /**
     * 为元素设置指定事件的侦听器。
     * 
     * @param event 要设置侦听器的事件 
     * @param handler 侦听器
     * @param options 与 EventTarget.addEventListener 的 options 参数相同
     * @return 被设置的侦听器，即参数 handler 本身
     */
    on: function(event: string, handler: ShEventListener<'default'>, options?: EventListenerOptions): ShEventListener<'default'> {
        (this as unknown as HTMLElement).addEventListener(event, handler, options)
        return handler
    },
    /**
     * 为元素删除指定事件的侦听器。
     * 
     * @param event 要删除侦听器的事件
     * @param handler 要删除的侦听器
     * @param options 与 EventTarget.removeEventListener 的 options 参数相同。
     */
    off: function(event: string, handler: ShEventListener<'default'>, options?: EventListenerOptions) {
        (this as unknown as HTMLElement).removeEventListener(event, handler, options)
    },
    /**
     * 将本元素注册，使得 clickout 事件可以发送到本元素。
     */
    enableClickout: function() {
        _clickoutWatchers.push(this as unknown as HTMLElement)
    },
    /**
     * 将本元素解注册，使得 clickout 事件不再发送到本元素。
     */
    disableClickout: function () {
        _clickoutWatchers = _clickoutWatchers.filter(v => (v !== (this as unknown as HTMLElement)))
    },
})

